package io.kiki.stack.http.feign.form;

import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import io.kiki.stack.http.feign.form.multipart.*;
import io.kiki.stack.http.form.multipart.*;
import lombok.experimental.FieldDefaults;
import lombok.val;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;

import static io.kiki.stack.http.feign.form.ContentType.MULTIPART;
import static lombok.AccessLevel.PRIVATE;

/**
 * Multipart form content processor.
 *
 * @author Artem Labazin
 */
@FieldDefaults(level = PRIVATE, makeFinal = true)
public class MultipartFormContentProcessor implements ContentProcessor {

    Deque<Writer> writers;

    Writer defaultPerocessor;

    /**
     * Constructor with specific delegate encoder.
     *
     * @param delegate specific delegate encoder for cases, when this processor couldn't handle
     *                 request parameter.
     */
    public MultipartFormContentProcessor(Encoder delegate) {
        writers = new LinkedList<>();
        addWriter(new ByteArrayWriter());
        addWriter(new FormDataWriter());
        addWriter(new SingleFileWriter());
        addWriter(new ManyFilesWriter());
        addWriter(new SingleParameterWriter());
        addWriter(new ManyParametersWriter());
        addWriter(new PojoWriter(writers));

        defaultPerocessor = new DelegateWriter(delegate);
    }

    @Override
    public void process(RequestTemplate template, Charset charset, Map<String, Object> data) throws EncodeException {
        val boundary = Long.toHexString(System.currentTimeMillis());
        try (val output = new Output(charset)) {
            for (val entry : data.entrySet()) {
                if (entry == null || entry.getKey() == null || entry.getValue() == null) {
                    continue;
                }
                val writer = findApplicableWriter(entry.getValue());
                writer.write(output, boundary, entry.getKey(), entry.getValue());
            }

            output.write("--").write(boundary).write("--").write(CRLF);

            val contentTypeHeaderValue = new StringBuilder().append(getSupportedContentType().getHeader()).append("; charset=").append(charset.name()).append("; boundary=").append(boundary).toString();

            template.header(CONTENT_TYPE_HEADER, Collections.<String>emptyList()); // reset header
            template.header(CONTENT_TYPE_HEADER, contentTypeHeaderValue);

            // Feign's clients try to determine binary/string content by charset presence
            // so, I set it to null (in spite of availability charset) for backward
            // compatibility.
            val bytes = output.toByteArray();
            template.body(bytes, null);
        } catch (IOException ex) {
            throw new EncodeException("Output closing error", ex);
        }
    }

    @Override
    public ContentType getSupportedContentType() {
        return MULTIPART;
    }

    /**
     * Adds {@link Writer} instance in runtime.
     *
     * @param writer additional writer.
     */
    public final void addWriter(Writer writer) {
        writers.add(writer);
    }

    /**
     * Adds {@link Writer} instance in runtime at the beginning of writers list.
     *
     * @param writer additional writer.
     */
    public final void addFirstWriter(Writer writer) {
        writers.addFirst(writer);
    }

    /**
     * Adds {@link Writer} instance in runtime at the end of writers list.
     *
     * @param writer additional writer.
     */
    public final void addLastWriter(Writer writer) {
        writers.addLast(writer);
    }

    /**
     * Returns the <b>unmodifiable</b> collection of all writers.
     *
     * @return writers collection.
     */
    public final Collection<Writer> getWriters() {
        return Collections.unmodifiableCollection(writers);
    }

    private Writer findApplicableWriter(Object value) {
        for (val writer : writers) {
            if (writer.isApplicable(value)) {
                return writer;
            }
        }
        return defaultPerocessor;
    }
}
